The approaches described in this section could be pursued, but are not necessarily approaches that have been previously conceived or pursued. Therefore, unless otherwise indicated herein, the approaches described in this section are not prior art to the claims in this application and are not admitted to be prior art by inclusion in this section.
A typical Java Virtual Machine (JVM) includes an interpreter for executing Java applications or other Java-based code. When a Java method implemented as bytecodes in an instance of a Java class is invoked, the interpreter accesses the method and executes the bytecodes interpretively. Some JVMs may further provide a dynamic adaptive compiler for speeding the execution of Java methods. When such JVM detects that a particular method is frequently executed, the JVM uses the dynamic adaptive compiler to compile the method into native code. The JVM stores the native code in a region of memory (e.g. a code cache), and the next time the method is invoked, the JVM executes the native code found in memory instead of using the interpreter to interpretively execute the bytecode of the method. In order to efficiently utilize its allocated memory, such JVM typically removes from memory the native code for compiled methods that are not used frequently anymore. The process of removing the executable code for a compiled method from memory is referred to herein as decompilation.
A typical method may include invocations, or calls, to a number of other methods or even to itself (e.g. recursive calls). A method that invokes another method is referred to herein as a calling method. A method that is being invoked by another method is referred to herein as a target method. In a typical JVM, method invocations from compiled methods are slower than necessary because the correct runtime addresses of the target methods are not known by the calling methods at compile time. For example, suppose that a JVM decides to compile a calling method that includes a call to a target method. At compilation time, the JVM does not know the runtime address of the target method with certainty because either (1) the compilation state of the target method (e.g. compiled or not compiled) may change after the calling method is compiled, or (2) the target method may be a virtual method that may be overridden by another method after the calling method is compiled. Because of this uncertainty, the JVM can determine the correct address of the target method only at runtime when the target method is invoked; thus, after compiling the calling method and storing its executable code in memory, the address of the target method has to be determined every time the calling method actually invokes the target method by performing a lookup in the method block of the target method that is stored in memory. (A method block of a loaded method typically indicates the type of the method and stores other run-time information about the method.)
This approach of invoking the target method may cause a significant slowdown on computer processors that have a pipelined architecture. In such processors, a pipeline is used to look forward and pre-fetch a subsequent instruction while the processor is decoding, executing, or writing the results from a previous instruction. In this way, a pipelined processor may be processing and executing several instructions in parallel—for example, while the results of a first instruction are being written to memory or to a register, the processor is executing a second instruction, decoding a third instruction, and fetching a fourth instruction. In the above approach of invoking a target method, the uncertainty of the address of the target method at the time a calling method is compiled may cause a processor to pre-fetch the wrong instructions during the execution of the calling method, which in turn would cause the processor to flush its pipeline. However, flushing the processor pipeline wastes processor cycles and causes an execution slowdown.
For example, when compiling the code of a calling method that invokes a target method, it is necessary to use indirect referencing because it is not known whether the target method will be compiled at the time the target method is called. In some processor architectures, the following set of instructions may be used in the compiled code of the calling method to invoke the target method through indirect referencing:
# the address of the method block of the target method is pre-stored in “a0”
mov lr, pc
ldr pc, [a0].
The first of the above instructions (“mov lr, pc”) stores the value of the program counter “pc” as the return address at which execution will continue after the target method has finished executing. For example, on ARM processor architectures the program counter “pc” would contain the address of the current instruction plus 8. The second instruction (“ldr pc, [a0]”) loads the current address of the target method into the program counter from the first word of the method block of the target method in memory. If at the time of execution the target method is compiled, then the current address of the target method (as stored in the first word of the method block of the target method) is the address of the target method in the code cache; if the target method is not compiled, then the current address of the target method is the address of some helper glue code that causes the execution to be redirected to the interpreter. The above set of instructions for invoking a target method performs poorly on a processor with pipelines because the processor cannot look ahead and determine the current address of the target method due to the indirect referencing. This may result in a complete processor pipeline flush every time the target method is invoked, and the number of processor cycles wasted may be equal to the depth of the pipeline.
In order to reduce the chances of processor pipeline flushes, past approaches for target method invocations have relied on making direct method calls to target methods that have already been compiled. However, a problem arises when a compiled target method changes state from compiled to not compiled, because after the target method is decompiled, a direct call in a calling method that invokes this target method does not point to the correct address anymore. In order to solve this problem, past approaches have typically relied on forced decompilation of compiled calling methods. Forced decompilation provides that whenever the state of a target method changes (for example, whenever a target method is overridden or decompiled), all compiled methods that include instructions for directly invoking that target method are decompiled.
Forced decompilation, however, has some significant disadvantages. One disadvantage is that forced decompilation is difficult to implement, especially for methods that are currently in execution. Another disadvantage of forced decompilation is that it is very costly in terms of the memory and processor cycles being used. When a method is decompiled, every other compiled method that invokes this method necessarily must be decompiled. Thus, a state change of a single target method may cause a cascade of decompilations of a significant number of compiled methods. Further, a decompiled method that is frequently invoked will have to eventually be recompiled, which causes the additional use of resources such as memory and processor cycles.
Based on the foregoing, there is a clear need for a technique that does not cause processor pipeline flushes and that avoids the disadvantages of forced decompilation described above.